<?php
/**
 * @package front-framework
 * @author  Stephen Adkins <spadkins@gmail.com>
 * @copyright Copyright 2010-2013 Internet Dynamics <info@internetdynamics.com>
 *
 * This software began development in 2010.
 * It is based on code dating from 1999.
 * All copyrights from all authors and contributors to this source file
 * have been assigned to Internet Dynamics.
 *
 * This source code is available from Internet Dynamics under two possible
 * licenses: a Free Software License (GPLv3) or a Commercial License.
 * If you have this software and have not paid for a Commercial License,
 * you are welcome to it under the terms of the Free Software (GPLv3) license.
 * The simple explanation of this is that if you use this PHP program file in
 * your own PHP program, then that whole program must by GPLv3 licensed.
 *
 * License: GPL 3.0
 * The contents of this file may be used under the terms of
 * the GNU General Public License Version 2 or later (the "GPL").
 * -- OR --
 * License: Internet Dynamics Commercial License
 * The contents of this file may be used under the terms of
 * the Internet Dynamics Commercial License for the use of a business or
 * entity if that business or entity has paid Internet Dynamics for
 * that commercial license.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the appropriate
 * license for the specific language governing rights and limitations under the
 * license.
 *
 * This package or library of software occupies the PHP 'App' namespace.
 * It is the framework developed for the Front Suite of software consisting of
 *
 *   * sitefront: PHP application at the front of your website to manage Apache htaccess settings and DNS entries (user authentication, redirects, load balancing, high availability instance management, subdomains, etc.)
 *   * dbfront: PHP application to act as a front end for relational databases. (create, store, use CRUD screens with fine-grained access control to create RIA database applications)
 *   * appfront: PHP application to act as an installation manager for multiple PHP applications including this Front Suite of applications. (install, upgrade, backup, restore, synchronize other applications)
 *
 * */
namespace App;

define('APP_GUEST_USER_ID',                    1);
define('APP_SYSTEM_USER_ID',                   2);
define('APP_GUEST_USERNAME',             'guest');
define('APP_SYSTEM_USERNAME',           'system');

define('APP_ALLUSERS_GROUP_ID',                1);
define('APP_SYSTEM_GROUP_ID',                  2);
define('APP_ALLUSERS_GROUP_NAME',    'all-users');
define('APP_SYSTEM_GROUP_NAME',         'system');

define('APP_ADMIN_ROLE_ID',                    1);
define('APP_COLLABORATOR_ROLE_ID',             2);
define('APP_OBSERVER_ROLE_ID',                 3);

define('APP_DEFAULT_APP_ID',                   1);
define('APP_DUMMY_AUTH_TOKEN',          'xxxxxx');
# define('APP_END_OF_TIME', '2038-01-19T03:14:07Z');   # this is the end of Unix time. time = 0x7FFFFFFF (= 2147483647) (highest signed 32-bit integer)
define('APP_END_OF_TIME', '2038-01-01 00:00:00');   # this is easier to remember and gives some wiggle room if some date math adds some hours before subtracting them. (= 0x7FE825CD = 2145920461)

define('ERROR_INIT_CONFIGPHP',                 1);
define('ERROR_INIT_AUTHDB',                    2);
define('ERROR_AUTH_NO_USER',                  10);
define('ERROR_AUTH_BAD_PASSWORD',             11);
define('ERROR_AUTH_NOCONNTOKEN',              12);
define('ERROR_AUTH_BADCONNTOKEN',             13);
define('ERROR_AUTH_NOUSERTOKEN',              14);
define('ERROR_AUTH_USERNOTFOUND',             15);
define('ERROR_AUTH_USERNOTCONFIRMED',         16);
define('ERROR_AUTH_USERBLOCKED',              17);
define('ERROR_AUTH_NOCAPTCHACHALLENGE',       18);
define('ERROR_AUTH_NOCAPTCHARESPONSE',        19);
define('ERROR_AUTH_BADCAPTCHARESPONSE',       20);
define('ERROR_AUTH_CREATENOUSERNAME',         21);
define('ERROR_AUTH_BADALGORITHM',             22);
define('ERROR_AUTH_NOAUTHTOKEN',              23);
define('ERROR_AUTH_SESSIONEXPIRED',           24);
define('ERROR_AUTH_GUESTUSERNOTFOUND',        25);
define('ERROR_AUTH_REGNOUSERTOKEN',           26);
define('ERROR_AUTH_RESETNOEMAIL',             27);
define('ERROR_AUTH_RESETNOCCTOKEN',           28);
define('ERROR_AUTH_RESETBADEMAIL',            29);
define('ERROR_AUTH_RESETBADCCTOKEN',          30);
define('ERROR_CONF_PARAMNOTSET',              40);

#############################################################################
# Context CLASS
#############################################################################

/**
 * App\Context
 *
 * An App\Context object is the singleton that represents the runtime context of the program and which acts as the factory for other objects in the Front Framework
 *
 * The application context is allocated by the frontstart.inc.php script and becomes available as 
 * a global variable ($context) along with the options array ($options).
 *
 * @copyright Copyright 2013 Internet Dynamics, http://www.internetdynamics.com
 * @license   GPLv3 (or later) (users may license this software commercially from Internet Dynamics if they need to use it under a license which does not have copyleft)
 * @author    Stephen Adkins <spadkins@internetdynamics.com>
 * @package   sitefront/front-framework
 * @uses      App\Context, App
 * @version   $Id$
 */
class Context {
    /**
     * @var array $options - the global option array. contains global settings and parameters from an options.ini file on disk.
     */
    protected $options;
    /**
     * @var App\Database $authdb - the authentication database (used to validate authentication credentials and determine access authorization)
     */
    protected $authdb;
    /**
     * @var array $conf - the service configuration tree. This might have been loaded from the global $prefix/etc/config.php or
     *                    from individual service configuration files such as $prefix/etc/Database/monitoring.php.
     */
    protected $conf    = array();
    /**
     * @var array $cache - the service cache
     *
     * All service instances of all service_types that have every been instantiate by the $context
     * using the $context->service(...) factory method are stored in the global service cache.
     * If any of those names services are needed again, it is very efficient to retrieve them from
     * the cache using the same call to the $context->service() method.
     */
    protected $cache   = array();

    protected $meta_session_id   = 0;        # unique meta-session ID
    protected $meta_session_send = 0;        # flag to save the meta-session just before flushing output
    protected $session           = array();  # user_token, auth_token, session_expire_dttm, group_id, app_id
    protected $session_send      = 0;        # flag to save the session just before flushing output

    protected $authenticated_user_token, $authenticated_user_id, $authenticated_user, $authenticated_group_id, $authenticated_app_id;
    protected $authorized_perms, $authorized_databases;
    protected $owner_id          = array();

    protected $request_id;
    protected $request_start_time, $request_run_end_time, $request_end_time;
    protected $request_start_realtime;

    protected $remote_addr, $remote_ip_num, $http_user_agent, $host, $proto, $request_method, $http_host, $server_port, $request_uri, $http_referer, $request_size;

    /**
     * @var int $no_cache - a flag that indicates if the "no-cache" headers have been sent. if so, we want to inhibit sending them again. (see $this->noCache())
     */
    protected $no_cache = 0;

    /**
     * @var float $run_db_time - the total time in seconds spent in database calls
     */
    protected $run_db_time       = 0.0;
    /**
     * @var int $num_db_calls - the total number of database calls made (select, insert, update, delete)
     */
    protected $num_db_calls      = 0;
    /**
     * @var int $num_db_rows_read - the total number of rows read (with select statements)
     */
    protected $num_db_rows_read  = 0;
    /**
     * @var int $num_db_rows_write - the total number of rows written (with insert, update, or delete statements)
     */
    protected $num_db_rows_write = 0;

    /**
     * __construct(&$options) the constructor is called by the frontstart.inc.php script, passing the global $options array
     *
     * The application context is allocated (i.e. this class is constructed and the __construct() method is called) by the
     * frontstart.inc.php script and $context becomes available as a global variable along with the options array ($options).
     *
     * @param  array &$options
     * @return void
     */
    public function __construct(&$options) {
        global $trace;
        if ($trace) trace_entry();

        $this->request_start_time = isset($_SERVER['REQUEST_TIME_FLOAT']) ? $_SERVER['REQUEST_TIME_FLOAT'] : microtime(true);
        
        if ($options['os'] === 'WIN') {
            $this->request_start_realtime = $this->request_start_time;  
        }
        else { 
            $rusage = getrusage(true);
            $this->request_start_realtime = $rusage['ru_utime.tv_sec']+$rusage['ru_utime.tv_usec']*1e-6;
        }     

        date_default_timezone_set('UTC');

        $this->setOptionDefaults($options);
        $this->options = $options;

        $matches = array();
        $this->remote_addr             = isset($_SERVER['REMOTE_ADDR'])     ? $_SERVER['REMOTE_ADDR']     : '';
        if ($this->remote_addr && preg_match('/^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)$/', $this->remote_addr, $matches)) {
            $this->remote_ip_num = $matches[1]*256*256*256 + $matches[2]*256*256 + $matches[3]*256 + $matches[4];
        }
        else $this->remote_ip_num = 0;
        $this->http_user_agent         = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
        $this->host                    = gethostname();
        $this->proto                   = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $this->request_method          = isset($_SERVER['REQUEST_METHOD'])  ? $_SERVER['REQUEST_METHOD']  : '';
        $this->http_host               = isset($_SERVER['HTTP_HOST'])       ? $_SERVER['HTTP_HOST']       : '';
        $this->server_port             = isset($_SERVER['SERVER_PORT'])     ? $_SERVER['SERVER_PORT']     : '';
        $this->request_uri             = isset($_SERVER['REQUEST_URI'])     ? $_SERVER['REQUEST_URI']     : '';
        $this->http_referer            = isset($_SERVER['HTTP_REFERER'])    ? $_SERVER['HTTP_REFERER']    : '';
        $this->request_size            = isset($_SERVER['CONTENT_LENGTH'])  ? $_SERVER['CONTENT_LENGTH']  : 0;

        $test_mode = isset($options['test_mode']) ? $options['test_mode'] : 0;

        # set these up so that attempts to use the 'auth' and 'log' databases are properly done
        $this->authenticated_user_token  = APP_GUEST_USERNAME;
        $this->authenticated_user_id   = APP_GUEST_USER_ID;
        $this->authenticated_group_id  = APP_ALLUSERS_GROUP_ID;

        $prefix = $options['prefix'];
        try {
            if (file_exists("$prefix/etc/config.php")) {
                $this->conf = include "$prefix/etc/config.php";
                #debug_print("[conf/config.php]=" . print_r($this->conf,true));
            }
        }
        catch (\Exception $e) {
            $details = $test_mode ? " ($prefix/etc/config.php) " . $e->getMessage() : " Please report this.";
            throw new \Exception("ERROR in startup. Perhaps there is a syntax error in the global config file.$details", ERROR_INIT_CONFIGPHP);
        }

        try {
            $this->authdb = $this->database('auth');
        }
        catch (\Exception $e) {
            $details = $test_mode ? " [$this->authdb] " . $e->getMessage() : " Please report this.";
            throw new \Exception("ERROR in startup. Perhaps there is an error connecting to the authentication database.$details", ERROR_INIT_AUTHDB);
        }

        $this->initializeSession();           // pull data in from the cookie (first use of 'log' database)
        $this->login();
        $this->beginRequestLog();

        if ($trace) trace_exit();
    }

    /**
     * setOptionDefaults(&$options) gets called by the constructor to ensure that sensible defaults exist for many option keys.
     *
     * The following are the defaults that are set.
     *
     * * $options['test_mode']     = 0
     * * $options['app.title']     = "FrontSuite"
     * * $options['app.namespace'] = "FrontSuite"
     * * $options['favicon']       = "favicon.ico"
     * * $options['ext_urldir']    = "ext"
     * * $options['ext_jsfile']    = "ext-all.js"
     * * $options['lang']          = "en"       // this is provided for general purposes. (not sure how it is to be used yet)
     * * $options['locale']        = "en"       // this one is actually used by ExtJS
     * * $options['timezone']      = "America/New_York"
     * * $options['theme']         = "default"
     *
     * @param array &$options
     * @return void
     */
    protected function setOptionDefaults (&$options) {
        if (!isset($options['test_mode']))       $options['test_mode']     = 0;
        if (!isset($options['app.title']))       $options['app.title']     = "FrontSuite";
        if (!isset($options['app.namespace']))   $options['app.namespace'] = "FrontSuite";
        if (!isset($options['favicon']))         $options['favicon']       = "favicon.ico";
        if (!isset($options['ext_urldir']))      $options['ext_urldir']    = "ext";
        if (!isset($options['ext_jsfile']))      $options['ext_jsfile']    = "ext-all.js";

        if     (isset($_REQUEST['lang']))        $options['lang']          = $_REQUEST['lang'];
        elseif (!isset($options['lang']))        $options['lang']          = "en";

        if     (isset($_REQUEST['locale']))      $options['locale']        = $_REQUEST['locale'];
        elseif (!isset($options['locale']))      $options['locale']        = "en";

        if     (isset($_REQUEST['timezone']))    $options['timezone']      = $_REQUEST['timezone'];
        elseif (!isset($options['timezone']))    $options['timezone']      = "America/New_York";

        if     (isset($_REQUEST['theme']))       $options['theme']         = $_REQUEST['theme'];
        elseif (!isset($options['theme']))       $options['theme']         = "default";
    }

    /**
     * options() retrieves the stored global $options array.
     * The global options array may be generally accessed two ways.
     * The preferrable way, if a section of code has access to $context, is to call "$options = $context->options();" to retrieve it.
     * The alternate way is simply to declare it as a global ("global $options;" in function scope) and then access it as "$options".
     * @param void
     * @return array $options
     */
    public function options () {
        global $trace;
        if ($trace) trace_entry();
        if ($trace) trace_exit($this->options);
        return($this->options);
    }

    /**
     * getOption($option_var, $default = null) retrieves a single option value from the global $options array.
     * @param string $option_var
     * @param string $default = null
     * @return string $option_value
     */
    public function getOption ($option_var, $default = null) {
        global $trace;
        if ($trace) trace_entry();
        $option_value = array_key_exists($option_var, $this->options) ? $this->options[$option_var] : $default;
        if ($trace) trace_exit($option_value);
        return($option_value);
    }

    /**
     * initializeSession() unpacks the data stored in the 'ff-meta-session' and 'ff-session' cookies.
     * Specifically, it gets the meta_session_id out of the ff-meta-session cookie. This id allows activity to be tracked on the server for requests even when the client has not logged in.
     * Also, ff-session cookie contains a JSON-encoded associative array of values, including the user_token, auth_token, session_expire_dttm.
     * All values in the ff-session are unpacked and stored in the local session array.
     * Because this session is stored in a cookie, there is a limit of 4K of serialized session space.
     * Therefore, although the application may also use this session, it should be used sparingly and carefully.
     * @param void
     * @return void
     */
    private function initializeSession () {
        global $trace, $options;
        if ($trace) trace_entry();
        $app_env_suffix = (isset($options['app_env']) && $options['app_env'] !== 'prod') ? '-'.$options['app_env'] : '';

        $logdb = $this->database('log');
        $client_connection_token = $this->authentication()->getClientConnectionToken();

        $ff_meta_session_cookie = 'ff-meta-session'.$app_env_suffix;
        if (isset($_COOKIE[$ff_meta_session_cookie])) {
            $cookie = $_COOKIE[$ff_meta_session_cookie];
            if ($cookie) {
                $this->meta_session_id = $cookie;  # unpack the cookie value. at present, it is just the one single value: meta_session_id.
                $meta_session_client_connection_token = $logdb->get('select client_connection_token from {log_schema_}meta_session where meta_session_id = ?', array($this->meta_session_id));
                if ($meta_session_client_connection_token !== $client_connection_token) {
                    $this->meta_session_id   = 0;
                    $this->meta_session_send = 1;
                }
            }
        }
        if (!$this->meta_session_id) {
            $recent_session_time = time()-30*60;
            $recent_session_dttm = date('Y-m-d H:i:s', $recent_session_time);
            $meta_session_id = $logdb->get('select meta_session_id from {log_schema_}meta_session where create_dttm >= ? and client_connection_token = ? order by create_dttm desc',
                array($recent_session_dttm, $client_connection_token));
            if (isset($meta_session_id)) {
                $this->meta_session_id   = $meta_session_id;
                $this->meta_session_send = 1;
            }
        }
        if (!$this->meta_session_id) {
            $logdb->execute('insert into {log_schema_}meta_session (remote_addr, remote_ip_num, http_user_agent, client_connection_token) values (?, ?, ?, ?)',
                array($this->remote_addr, $this->remote_ip_num, $this->http_user_agent, $client_connection_token));
            $this->meta_session_id   = $logdb->last_insert_id('meta_session');
            $this->meta_session_send = 1;
        }

        $ff_session_cookie = 'ff-session'.$app_env_suffix;
        if (isset($_COOKIE[$ff_session_cookie])) {
            $cookie = $_COOKIE[$ff_session_cookie];
            $cookie = str_replace('\\"','"',$cookie);    # Ouch. Temporary work-around. PHP 5.3.5 json_decode() on Mac barfs on the escape characters.
            $ff_session = json_decode($cookie, true);
            foreach ($ff_session as $var => $value) {    # user_token, session_expire_dttm, auth_token, group_id, app_id
                $this->session[$var] = $value;
            }
        }
        if ($trace) trace_exit();
    }

    /**
     * modifySession() modifies the values in the session maintained in the 'ff-session' cookie and flags the data for retransmission to the browser
     * This causes the session cookie to be modified and returned.
     * @param array $values
     * @return void
     */
    public function modifySession ($values = null) {
        global $trace;
        if ($trace) trace_entry();
        if (isset($values)) {
            foreach ($values as $var => $value) {    # user_token, session_expire_dttm, auth_token, group_id, app_id
                if ($value === '') {
                    if (isset($this->session[$var])) unset($this->session[$var]);
                }
                else {
                    $this->session[$var] = $value;
                }
            }
        }
        $this->session_send = 1;
        if ($trace) trace_exit();
    }

    /**
     * getSession() retrieves the session stored in the Context (simple getter)
     * @param void
     * @return array $session
     */
    public function getSession () {
        return($this->session);
    }

    /**
     * setLoginAction() sets up a login action
     * This causes the session cookie to be modified and returned.
     * It also causes javascript to be emitted which sets the values in the FrontSuite.session in the page currently being generated.
     * @param string $login_action
     * @param string $login_arg
     * @return void
     */
    public function setLoginAction ($login_action, $login_arg) {
        global $trace;
        if ($trace) trace_entry();
        if (preg_match('/^[a-z_]+$/', $login_action) &&
            preg_match('/^[^\'"<>]+$/', $login_arg)) {
            $this->modifySession(array('login_action' => $login_action, 'login_arg' => $login_arg));
            echo <<<EOF
<script type="text/javascript">
    if (!window.FrontSuite) FrontSuite = {};
    if (!FrontSuite.session) FrontSuite.session = {};
    FrontSuite.session.login_action = "$login_action";
    FrontSuite.session.login_arg = "$login_arg";
</script>
EOF;
        }
        if ($trace) trace_exit();
    }

    /**
     * endRequest() calls the PHP ob_end_flush() (so hopefully the script called ob_start() at the beginning) but before that, it spits out whatever headers may have been deferred (e.g. the ff-meta-session and ff-session cookies)
     * Emit the ff-meta-session and ff-session cookies (if they have been modified and need to be sent), grab some end-of-request request metrics, and flush the PHP page output buffer.
     * Any PHP script that uses this framework should call "ob_start();" at its beginning and call $context->endRequest() at its end.
     * @param void
     * @return void
     */
    public function endRequest () {
        global $trace, $options, $debug;
        if ($trace) trace_entry();
        $this->sendFFMetaSession();
        $this->sendFFSession();
        $this->response_size = ob_get_length();
        $this->request_run_end_time = microtime(true);
        ob_end_flush();
        $this->request_end_time = microtime(true);
        $this->endRequestLog();
        if ($trace) trace_exit();
    }

    /**
     * sendFFMetaSession() - calls setcookie for the ff-meta-session cookie
     * @param void
     * @return void
     */
    public function sendFFMetaSession () {
        global $trace, $options, $debug;
        if ($trace) trace_entry();
        if ($this->meta_session_send) {
            $app_env_suffix = (isset($options['app_env']) && $options['app_env'] !== 'prod') ? '-'.$options['app_env'] : '';
            $ff_meta_session_cookie = 'ff-meta-session'.$app_env_suffix;
            $global_cookie_domain = $this->options['global_cookie_domain'];
            $cookie_secure        = (isset($options['cookie_secure']) && $options['cookie_secure']) ? true : false;
            $cookie_data = $this->meta_session_id;
            $expire = time()+60*60*24*365*10;   # expire in 10 years ("forever")
            # bool setcookie ( string $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, bool $secure = false [, bool $httponly = false ]]]]]] )
            setcookie($ff_meta_session_cookie, $cookie_data, $expire, '/', $global_cookie_domain, $cookie_secure, true);
            if ($debug) {
                $session_expire_dttm = date('Y-m-d H:i:s', $expire);
                $cookie_secure_str = $cookie_secure ? 'true' : 'false';
                debug_print("setcookie($ff_meta_session_cookie, $cookie_data, $session_expire_dttm, '/', $global_cookie_domain, $cookie_secure_str, true);");
            }
        }
        if ($trace) trace_exit();
    }

    /**
     * sendFFSession() - calls setcookie for the ff-session cookie
     * @param void
     * @return void
     */
    public function sendFFSession () {
        global $trace, $options, $debug;
        if ($trace) trace_entry();
        if ($this->session_send) {
            $app_env_suffix       = (isset($options['app_env']) && $options['app_env'] !== 'prod') ? '-'.$options['app_env'] : '';
            $ff_session_cookie    = 'ff-session'.$app_env_suffix;
            $global_cookie_domain = $this->options['global_cookie_domain'];
            $cookie_secure        = (isset($options['cookie_secure']) && $options['cookie_secure']) ? true : false;
            if (isset($this->session['session_expire_dttm'])) {
                $session_expire_dttm = $this->session['session_expire_dttm'];
                $expire = strtotime($session_expire_dttm);
            }
            else {
                $cookie_expire_days   = (isset($options['cookie_expire_days'])) ? $options['cookie_expire_days'] : 30;
                $expire = time()+$cookie_expire_days*24*3600;
                #$expire = 0;   # expire when the browser is closed
                $session_expire_dttm = date('Y-m-d H:i:s', $expire);
                $this->session['session_expire_dttm'] = $session_expire_dttm;
            }
            $cookie_data          = json_encode($this->session);
            # bool setcookie ( string $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, bool $secure = false [, bool $httponly = false ]]]]]] )
            setcookie($ff_session_cookie, $cookie_data, $expire, '/', $global_cookie_domain, $cookie_secure, true);
            if ($debug) {
                $cookie_secure_str = $cookie_secure ? 'true' : 'false';
                debug_print("setcookie($ff_session_cookie, $cookie_data, $session_expire_dttm, '/', $global_cookie_domain, $cookie_secure_str, true);");
            }
        }
        if ($trace) trace_exit();
    }

    /**
     * login()
     * This method is called internally.
     * It uses the "authentication" service to get
     * @param void
     * @return void
     */
    public function login ($user_token = null, $auth_token = null, $session_expire_dttm = null, $group_id = null, $app_id = null) {
        global $trace;
        if ($trace) trace_entry();

        $initial_user_token                                      = (isset($this->session['user_token']))          ? $this->session['user_token']          : APP_GUEST_USERNAME;
        if (!isset($user_token))          { $user_token          = $initial_user_token; }
        if (!isset($auth_token))          { $auth_token          = (isset($this->session['auth_token']))          ? $this->session['auth_token']          : APP_DUMMY_AUTH_TOKEN; }
        if (!isset($session_expire_dttm)) { $session_expire_dttm = (isset($this->session['session_expire_dttm'])) ? $this->session['session_expire_dttm'] : APP_END_OF_TIME; }
        if (!isset($group_id))            { $group_id            = (isset($this->session['group_id']))            ? $this->session['group_id']            : APP_ALLUSERS_GROUP_ID; }
        if (!isset($app_id))              { $app_id              = (isset($this->session['app_id']))              ? $this->session['app_id']              : APP_DEFAULT_APP_ID; }

        $auth = $this->authentication();
        $user = $auth->getAuthenticatedUser($user_token, $auth_token, $session_expire_dttm);

        # if we are just logging in, change our group to our home group...
        if ($initial_user_token === 'guest' && $user['user_id'] != APP_ALLUSERS_GROUP_ID && $group_id == APP_ALLUSERS_GROUP_ID) {
            $group_id = $user['user_id'];
        }

        $this->getConsistentUserInfo($user, $group_id, $app_id);
        if ($user_token !== $user['user_token']) {
            $user_token                 = $user['user_token'];
            $auth_token                 = APP_DUMMY_AUTH_TOKEN;
            $session_expire_dttm        = APP_END_OF_TIME;
        }
        $group_id                       = $user['group_id'];
        $app_id                         = $user['app_id'];

        $this->authenticated_user       = $user;
        $this->authenticated_user_token = $user['user_token'];
        $this->authenticated_user_id    = $user['user_id'];
        $this->authenticated_group_id   = $user['group_id'];
        $this->authenticated_app_id     = $user['app_id'];

        if (!isset($this->session['user_token']) || $this->session['user_token'] !== $user_token) {
            $this->session['user_token'] = $user_token;
            $this->session_send = 1;
            if (isset($this->session['login_action'])) { unset($this->session['login_action']); }
            if (isset($this->session['login_arg']))    { unset($this->session['login_arg']); }
        }
        if (!isset($this->session['auth_token']) || $this->session['auth_token'] !== $auth_token) {
            $this->session['auth_token'] = $auth_token;
            $this->session_send = 1;
        }
        if (!isset($this->session['session_expire_dttm']) || $this->session['session_expire_dttm'] !== $session_expire_dttm) {
            $this->session['session_expire_dttm'] = $session_expire_dttm;
            $this->session_send = 1;
        }
        if (!isset($this->session['group_id']) || $this->session['group_id'] != $group_id)   {
            $this->session['group_id'] = $group_id;  
            $this->session_send = 1;
        }
        if (!isset($this->session['app_id']) || $this->session['app_id'] != $app_id)     {
            $this->session['app_id'] = $app_id;    
            $this->session_send = 1;
        }

        if ($trace) trace_exit();
    }

    /**
     * switchUser()
     * This method is called internally.
     * @param void
     * @return void
     */
    public function switchUser ($user_id, $group_id = null, $app_id = null) {
        global $trace;
        if ($trace) trace_entry();

        $initial_user_token = (isset($this->session['user_token'])) ? $this->session['user_token'] : APP_GUEST_USERNAME;

        $authdb = $this->authdb();
        if ($user_id == $this->authenticated_user_id) {
            $user = $this->authenticated_user;
        }
        else {
            $user = $authdb->get_hash("select * from {auth_schema_}auth_user where user_id = ?", array($user_id));
        }
        if (!isset($group_id))  { $group_id = (isset($this->session['group_id'])) ? $this->session['group_id'] : $user_id; }
        if (!isset($app_id))    { $app_id   = (isset($this->session['app_id']))   ? $this->session['app_id']   : APP_DEFAULT_APP_ID; }
        #debug_print("switchUser($user_id, $group_id, $app_id): BEGIN: uid=$this->authenticated_user_id gid=$this->authenticated_group_id aid=$this->authenticated_app_id");

        $this->getConsistentUserInfo($user, $group_id, $app_id);

        $group_id                       = $user['group_id'];
        $app_id                         = $user['app_id'];

        $this->authenticated_user       = $user;
        $this->authenticated_user_token = $user['user_token'];
        $this->authenticated_user_id    = $user['user_id'];
        $this->authenticated_group_id   = $user['group_id'];
        $this->authenticated_app_id     = $user['app_id'];

        if (!isset($this->session['user_token']) || $this->session['user_token'] !== $user_token) {
            $this->session['user_token'] = $user_token;
            $this->session_send = 1;
            if (isset($this->session['login_action'])) { unset($this->session['login_action']); }
            if (isset($this->session['login_arg']))    { unset($this->session['login_arg']); }
        }
        if (!isset($this->session['auth_token']) || $this->session['auth_token'] !== $auth_token) {
            $this->session['auth_token'] = $auth_token;
            $this->session_send = 1;
        }
        if (!isset($this->session['session_expire_dttm']) || $this->session['session_expire_dttm'] !== $session_expire_dttm) {
            $this->session['session_expire_dttm'] = $session_expire_dttm;
            $this->session_send = 1;
        }
        if (!isset($this->session['group_id']) || $this->session['group_id'] != $group_id)   {
            $this->session['group_id'] = $group_id;  
            $this->session_send = 1;
        }
        if (!isset($this->session['app_id']) || $this->session['app_id'] != $app_id)     {
            $this->session['app_id'] = $app_id;    
            $this->session_send = 1;
        }

        #debug_print("switchUser($user_id, $group_id, $app_id): END    uid=$this->authenticated_user_id gid=$this->authenticated_group_id aid=$this->authenticated_app_id");
        if ($trace) trace_exit();
    }

    /**
     * beginRequestLog()
     * @param array $request
     * @param array &$response
     * @return void
     */
    private function beginRequestLog () {
        global $trace;
        if ($trace) trace_entry();
        $logdb                   = $this->database('log');
        $start_dttm              = date('Y-m-d H:i:s', intval($this->request_start_time));
        $start_mem_mb            = round(memory_get_usage()/(1024*1024),3);
        $logdb->execute('insert into {log_schema_}request (remote_addr, remote_ip_num, meta_session_id, user_id, group_id, host, proto, request_method, http_host, server_port, request_uri, http_referer, start_dttm, request_size, start_mem_mb) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
            array($this->remote_addr, $this->remote_ip_num, $this->meta_session_id, $this->authenticated_user_id, $this->authenticated_group_id, $this->host, $this->proto, $this->request_method, $this->http_host, $this->server_port, $this->request_uri, $this->http_referer, $start_dttm, $this->request_size, $start_mem_mb));
        $this->request_id        = $logdb->last_insert_id('request');
        if ($trace) trace_exit();
    }

    /**
     * endRequestLog()
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function endRequestLog () {
        global $trace, $options;
        if ($trace) trace_entry();
        $logdb                   = $this->database('log');

        if ($options['os'] === 'WIN') {
            $request_end_realtime = microtime(true);  
        }
        else {
            $rusage                  = getrusage(true);
            $request_end_realtime    = $rusage['ru_utime.tv_sec']+$rusage['ru_utime.tv_usec']*1e-6;
        }     

        $end_dttm                = date('Y-m-d H:i:s', intval($this->request_end_time));
        $end_mem_mb              = round(memory_get_peak_usage()/(1024*1024),3);
        $http_status             = http_response_code();
        $cpu_time                = $request_end_realtime - $this->request_start_realtime;
        $run_time                = round($this->request_end_time - $this->request_start_time,6);
        $run_xfer_time           = round($this->request_end_time - $this->request_run_end_time,6);
        $run_main_time           = $run_time - $this->run_db_time - $run_xfer_time;

        $logdb->execute('update {log_schema_}request set end_dttm = ?, end_mem_mb = ?, http_status = ?, response_size = ?, cpu_time = ?, run_time = ?, run_main_time = ?, run_db_time = ?, run_xfer_time = ?, num_db_calls = ?, num_db_rows_read = ?, num_db_rows_write = ? where request_id = ?',
            array($end_dttm, $end_mem_mb, $http_status, $this->response_size, $cpu_time, $run_time, $run_main_time, $this->run_db_time, $run_xfer_time, $this->num_db_calls, $this->num_db_rows_read, $this->num_db_rows_write, $this->request_id));
        if ($trace) trace_exit();
    }

    /**
     * accumulateDatabaseActivity($run_db_time, $num_db_calls, $num_db_rows_read, $num_db_rows_write)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function accumulateDatabaseActivity ($run_db_time, $num_db_calls, $num_db_rows_read, $num_db_rows_write) {
        global $trace;
        if ($trace) trace_entry();
        $this->run_db_time       += $run_db_time;
        $this->num_db_calls      += $num_db_calls;
        $this->num_db_rows_read  += $num_db_rows_read;
        $this->num_db_rows_write += $num_db_rows_write;
        if ($trace) trace_exit();
    }

    /**
     * getAuthenticatedUserToken()
     * @param void
     * @return string
     */
    public function getAuthenticatedUserToken () { return($this->authenticated_user_token); } 
    /**
     * getAuthenticatedUserId()
     * @param void
     * @return integer
     */
    public function getAuthenticatedUserId ()   { return($this->authenticated_user_id);   }
    /**
     * getAuthenticatedUser()
     * @param void
     * @return array
     */
    public function getAuthenticatedUser ()     { return($this->authenticated_user);     }
    /**
     * getAuthenticatedGroupId()
     * @param void
     * @return integer
     */
    public function getAuthenticatedGroupId ()  { return($this->authenticated_group_id);  }
    /**
     * getAuthenticatedAppId()
     * @param void
     * @return integer
     */
    public function getAuthenticatedAppId ()    { return($this->authenticated_app_id);    }

    /**
     * isAuthorized($perm)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function isAuthorized ($perm) {
        global $trace;
        if ($trace) trace_entry();
        if (!isset($this->authorized_perms)) $this->authorized_perms = $this->getAuthorizedPerms();
        $authorized = 0;
        if ($perm === '') $perm = '/';
        if (array_key_exists($perm, $this->authorized_perms)) {
            $authorized = $this->authorized_perms[$perm];
        }
        elseif ($perm === '/') {
            $authorized = 0;
        }
        else {
            $parent_perm = preg_replace('!/[^/]*$!','',$perm);
            if (strcmp($perm, $parent_perm) != 0) {
                $authorized = $this->isAuthorized($parent_perm);
                $this->authorized_perms[$perm] = $authorized;   # save the result for next time
            }
        }
        if ($trace) trace_exit($authorized);
        return($authorized);
    }

    /**
     * authdb()
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function authdb () {
        global $trace;
        if ($trace) trace_entry();
        #echo "Context::authdb() authdb=[$this->authdb]\n";
        if ($trace) trace_exit($this->authdb);
        return($this->authdb);
    }

    /**
     * metadb()
     * @param  integer      group_id
     * @param  string       db_env     [optional]
     * @return App\Database metadb
     */
    public function metadb ($owner_group_id = null, $db_env = null) {
        global $trace;
        if ($trace) trace_entry();
        if (!isset($owner_group_id)) $owner_group_id = $this->authenticated_group_id;
        #$name = ($owner_group_id == $this->authenticated_group_id) ? 'meta' : "meta-$owner_group_id";
        $name = "meta-$owner_group_id";
        if (!isset($db_env)) $db_env = $this->db_env();
        $metadb = $this->database($name, array('owner_group_id'=>$owner_group_id, 'db_env'=>$db_env, 'db_name'=>'meta'));
        if ($trace) trace_exit($metadb);
        return($metadb);
    }

    /**
     * getPublicDir($subdir)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function getPublicDir ($subdir = null) {
        global $trace, $options;
        if ($trace) trace_entry();
        $dir = $options['prefix'] . '/data/fs/pub';
        if (isset($subdir)) $dir .= $subdir;
        if (! is_dir($dir)) {
            $this->mkdir($dir);
        }
        if ($trace) trace_exit($dir);
        return($dir);
    }

    /**
     * getSystemDir($subdir)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function getSystemDir ($subdir = null) {
        global $trace, $options;
        if ($trace) trace_entry();
        $dir = $options['prefix'] . '/data/fs/sys';
        if (isset($subdir)) $dir .= $subdir;
        if (! is_dir($dir)) {
            $this->mkdir($dir);
        }
        if ($trace) trace_exit($dir);
        return($dir);
    }

    /**
     * getOwnerId($owner_name)
     */
    public function getOwnerId ($owner_name) {
        global $trace, $options;
        if ($trace) trace_entry();
        if ($owner_name === $this->authenticated_user['username']) {
            $owner_id = $this->authenticated_user_id;
        }
        elseif (isset($this->owner_id[$owner_name])) {
            $owner_id = $this->owner_id[$owner_name];
        }
        else {
            $authdb   = $this->authdb();
            $owner_id = $authdb->get("select owner_id from {auth_schema_}auth_owner where owner_name = ?", array($owner_name));
            $this->owner_id[$owner_name] = $owner_id;
        }
        if ($trace) trace_exit($owner_id);
        return($owner_id);
    }

    /**
     * getOwnerDir($subdir = null, $owner_group_id = null)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function getOwnerDir ($subdir = null, $owner_group_id = null) {
        global $trace, $options;
        if ($trace) trace_entry();
        if (! isset($owner_group_id)) {
            $owner_group_id = $this->getAuthenticatedGroupId();
        }
        if ($owner_group_id == APP_ALLUSERS_GROUP_ID) {
            $dir = $this->getPublicDir();
        }
        elseif ($owner_group_id == APP_SYSTEM_GROUP_ID) {
            $dir = $this->getSystemDir();
        }
        else {
            $dir = $options['prefix'] . '/data/fs' .
                   '/' . substr($owner_group_id,0,2) .
                   '/' . substr($owner_group_id,0,4) .
                   '/' . substr($owner_group_id,0,6) .
                   '/' . $owner_group_id;
            if (! is_dir($dir)) $this->mkdir($dir);
        }
        if (isset($subdir)) {
            $dir .= $subdir;
            if (! is_dir($dir)) $this->mkdir($dir);
        }
        if ($trace) trace_exit($dir);
        return($dir);
    }

    /**
     * service($service_type, $name = 'default', $code_conf = null)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function service ($service_type, $name = 'default', $code_conf = null) {
        global $trace;
        if ($trace) trace_entry();
        $prefix = $this->options['prefix'];
        #echo "service($service_type, $name, $code_conf)\n";
        $anonymous = ($name === 'anonymous') ? true : false;

        if (!$anonymous && isset($this->cache[$service_type]) && isset($this->cache[$service_type][$name])) {
            $service = $this->cache[$service_type][$name];
            #echo "service $service_type.$name found in cache [$service]<br>\n";
        }
        else {
            if (!isset($this->conf[$service_type])) {
                $this->conf[$service_type] = array();
            }
            if (isset($this->conf[$service_type][$name])) {          # if the conf is already known, just retrieve it
                $service_conf = $this->conf[$service_type][$name];
                #echo "creating service $service_type.$name: config found [$service_conf] in conf structure<br>\n";
            }
            elseif (file_exists("$prefix/etc/$service_type/$name.php")) {   # if the conf is in a service-specific file, read that in
                $service_conf = include "$prefix/etc/$service_type/$name.php";
                #echo "creating service $service_type.$name: config found [$service_conf] at [conf/$service_type/$name.php]<br>\n";
                $this->conf[$service_type][$name] = $service_conf;
            }
            else {                                                   # otherwise, just initialize it with an empty array
                #echo "creating service $service_type.$name: no config found [" . getcwd() . "/conf/$service_type/$name.php]<br>\n";
                $service_conf = array();
                $this->conf[$service_type][$name] = $service_conf;
            }
            if (is_array($code_conf)) {
                $service_conf = array_merge($code_conf, $service_conf);  # NOTE: conf from file overrides conf from code
            }

            $qualified_class = isset($service_conf['class']) ? $service_conf['class'] : "App\\$service_type";
            #echo "service_type=[$service_type] name=[$name] qualified_class=[$qualified_class]<br>\n";

            # Note: the following two lines are not needed because of the __autoload() function
            #$class = preg_replace('/^App\\\\/','',$qualified_class);
            #include "applib/App/$class.php";

            try {
                $service = new $qualified_class($this, $service_type, $name, $service_conf);
            }
            catch (Exception $e) {
                throw new \Exception("ERROR: cannot create a service($service_type,$name) : ". $e->getMessage());
            }
            if (!$anonymous) $this->cache[$service_type][$name] = $service;
        }
        if ($trace) trace_exit($service);
        return($service);
    }

    public function publicOptions () {
        return(array('app.title', 'theme', 'locale', 'timezone', 'apps_urldir', 'ext_urldir', 'ext_jsfile', 'test_mode', 'signup_mode',
                     'online_mode', 'site_baseurl', 'members_root_urldir', 'website_root_urldir', 'desktop_root_urldir', 'mobile_root_urldir', 'app_title'));
    }

    /**
     * initialJavascriptApplicationState()
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function initialJavascriptApplicationState () {
        $options     = $this->options;
        $namespace   = $options['app.namespace'];
        $apps_urldir = $options['apps_urldir'];
        $js = <<<EOF
    <script type="text/javascript">
        if (!window.$namespace) $namespace = {};   // create our top-level namespace
        $namespace.options    = {};                // options    is used to store config values from options.ini or perhaps \$_REQUEST or perhaps defaults
        $namespace.connection = {};                // connection is used to store connection-specific values 
        $namespace.session    = {};                // session    is used to store application data relevant to this session (gets lost on reload obviously)

EOF;
        foreach ($this->publicOptions() as $var) {
            if (isset($options[$var]))
                $js .= "        $namespace.options['$var'] = '$options[$var]';\n";
        }

        $authdata = array();
        $authdata['user_token'] = $this->getAuthenticatedUserToken();
        $authdata['user']       = $this->getAuthenticatedUser();
        $authdata['groups']     = $this->getAuthorizedGroups();
        $authdata['apps']       = $this->getAuthorizedApps();
        $authdata['perms']      = $this->getAuthorizedPerms();

        if (count($this->session) > 0) {
            foreach ($this->session as $var => $value) {
                $js .= "        $namespace.session.$var = '$value';\n";
            }
        }
        else {
            $js .= "        $namespace.session.user_token = '" . $this->getAuthenticatedUserToken() . "';\n";
            $js .= "        $namespace.session.user_id  = "  . $this->getAuthenticatedUserId()   . ";\n";
            $js .= "        $namespace.session.group_id = "  . $this->getAuthenticatedGroupId()  . ";\n";
            $js .= "        $namespace.session.app_id   = "  . $this->getAuthenticatedAppId()    . ";\n";
        }
        $js .= "        $namespace.session.authdata = ";
        $js .= json_encode($authdata);
        $js .= ";\n";

        $client_connection_token = $this->authentication()->getClientConnectionToken();
        $js .= "        $namespace.connection.client_connection_token = '$client_connection_token';\n";
        $js .= "    </script>\n";

        return($js);
    }

    /**
     * db_env()
     * Returns the current db_env (database environment). This is stored in the session cookie (ff-session).
     * @returns string  $db_env - the current db_env (e.g. 'prod', 'test', 'dev')
     */
    public function db_env () {
        global $trace;
        if ($trace) trace_entry();
        $db_env = 'prod';
        if ($trace) trace_exit($db_env);
        return($db_env);
    }

    /**
     * get_database_defs($options)
     * Returns the list of all databases that the user can access.
     * @param array $options - (none currently)
     * @returns array  $defs - a list (numerically indexed array) of associative arrays describing the databases
     */
    public function get_database_defs ($options = null) {
        global $trace;
        if ($trace) trace_entry();
        $authdb    = $this->authdb();
        $db_env    = $this->db_env();
        $group_id  = $this->getAuthenticatedGroupId();
        $db_select = 'select db.db_id, db.owner_group_id, db.db_env, db.db_name, db.db_label, db.db_desc, db.db_connection_id, ' .
                     'dbc.db_instance_id, dbc.db_connection_name, db.db_owner_group_id, dbc.default_schema, ' .
                     'db.dbtype, db.schema_name, db.status, ' .
                     'dbc.dbhost, dbc.db_class, dbc.db_class as class, dbc.dsn, dbc.dbuser, dbc.dbpass, dbc.dboptions, ' .
                     'db.reserved_size, db.est_current_size, db.last_refresh_dttm, db.user_mode, db.group_mode, db.other_mode ' .
                     'from {auth_schema_}db inner join {auth_schema_}db_connection dbc on dbc.db_connection_id = db.db_connection_id';
        #$database_defs = $authdb->get_hashes("$db_select where db.db_env = ? and db.owner_group_id = ? union $db_select where db.other_mode > 0", array($db_env, $group_id));
        $database_defs = $authdb->get_hashes("$db_select where db.db_env = ? and db.owner_group_id = ? and db.db_name not in ('auth','members','log','meta','group_private','user_private')", array($db_env, $group_id));
        if ($trace) trace_exit($database_defs);
        return($database_defs);
    }

    /**
     * get_database_def($name, $options)
     * Returns the list of all databases that the user can access.
     * @param string $name - name of the database
     * @param array $options - (none currently)
     * @returns array  $defs - a list (numerically indexed array) of associative arrays describing the databases
     */
    public function get_database_def ($name, $options) {
        global $trace;
        if ($trace) trace_entry();
        $authdb    = $this->context->authdb();
        $db_env    = $this->db_env();
        $group_id  = $this->getAuthenticatedGroupId();
        $db_select = 'select db.db_id, db.owner_group_id, db.db_env, db.db_name, db.db_label, db.db_desc, db.db_connection_id, ' .
                     'dbc.db_instance_id, dbc.db_connection_name, db.db_owner_group_id, dbc.default_schema, ' .
                     'db.dbtype, db.schema_name, db.status, ' .
                     'dbc.dbhost, dbc.db_class, dbc.db_class as class, dbc.dsn, dbc.dbuser, dbc.dbpass, dbc.dboptions, ' .
                     'db.reserved_size, db.est_current_size, db.last_refresh_dttm, db.user_mode, db.group_mode, db.other_mode ' .
                     'from {auth_schema_}db inner join {auth_schema_}db_connection dbc on dbc.db_connection_id = db.db_connection_id';
        $database_defs = $authdb->get_hash("$db_select where db.db_env = ? and db.db_name = ? and db.owner_group_id = ? union $db_select where db.db_env = 'prod' and db.db_name = ? and db.other_mode > 0", array($db_env, $db_name, $group_id, $db_name));
        if ($trace) trace_exit($database_defs);
        return($database_defs);
    }

    /**
     * database ($name = 'default', $code_conf = array())
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function database ($name = 'default', $code_conf = array()) {
        global $trace;
        if ($trace) trace_entry();

        $service_type = 'Database';
        if ($name === 'auth' && isset($this->authdb)) {
            $db = $this->authdb;
        }
        elseif (isset($this->cache[$service_type]) && isset($this->cache[$service_type][$name])) {
            $db = $this->cache[$service_type][$name];
        }
        else {
            $app_env = $this->options['app_env'];
            $group_id = $this->authenticated_group_id;

            if (isset($code_conf['owner_group_id']) && $code_conf['owner_group_id']) {
                $owner_group_id = $code_conf['owner_group_id'];
            }
            elseif ($group_id && ($name !== 'log' && $name !== 'auth' && $name !== 'members')) {
                $owner_group_id = $group_id;
                $code_conf['owner_group_id'] = $owner_group_id;
            }
            else {
                $owner_group_id = APP_ALLUSERS_GROUP_ID;
                $code_conf['owner_group_id'] = $owner_group_id;
            }

            if (isset($code_conf['db_env'])         && $code_conf['db_env'])         { $db_env = $code_conf['db_env']; }
            else                                                                     { $db_env = $this->db_env(); $code_conf['db_env'] = $db_env; }
            if (isset($code_conf['db_name'])        && $code_conf['db_name'])        { $db_name = $code_conf['db_name']; }
            else                                                                     { $db_name = $name; $code_conf['db_name'] = $db_name; }

            # We handle the 'auth' database differently.
            # That's because we use the auth database to initialize every other database.
            if ($name === 'auth') {
                $code_conf = Array('app_env' => $app_env, 'db_env' => $db_env,
                                    'dbtype' => 'sqlite', 'class'  => 'App\\SqlitePDODatabase',
                                        'label' => $name, 'db_name' => $db_name,
                               'owner_group_id' => $owner_group_id);
                $db = $this->service($service_type, $name, $code_conf);    # connect to the auth database. this will pick up config options from {env}/etc/config.php if available.
                $authdb = $db;   # objects are assigned by reference (instead of by value) automatically in PHP 5
                $this->authdb = $authdb;
                if (!isset($db->db_id)) {
                    $admin   = $this->sessionObject('administration', array('class' => 'App\\Administration'));
                    $conf    = $db->getConf();
                    $admin->lookupAndUpdateDatabaseConf('auth', $conf);
                    $db->db_id = $conf['db_id'];
                    $db->owner_group_id = $conf['owner_group_id'];
                }
            }
            else {
                $authdb = $this->authdb;
                $needs_db_conf = 0;
    
                if (!isset($code_conf['dbtype'])) $code_conf['dbtype'] = 'sqlite';
                if (!isset($code_conf['db_class'])) {
                    $dbtype = $code_conf['dbtype'];
                    if     ($dbtype === 'sqlite') $code_conf['db_class'] = 'App\\SqlitePDODatabase';
                    elseif ($dbtype === 'mysql')  $code_conf['db_class'] = 'App\\MySQLPDODatabase';
                    elseif ($dbtype === 'oracle') $code_conf['db_class'] = 'App\\OraclePDODatabase';
                    else   throw new \Exception("Can't create database ($name): db_class parameter not supplied");
                }
                $code_conf['class'] = $code_conf['db_class'];
                if (isset($code_conf['db_label']) && $code_conf['db_label']) $code_conf['label'] = $code_conf['db_label'];
    
                if (isset($code_conf['create'])) {
                    try {
                        $code_conf['owner_group_id'] = $group_id;
                        $authdb->insert("db", $code_conf);
                        $code_conf['db_id'] = $authdb->last_insert_id('db');
                    }
                    catch (Exception $e) {
                        throw new \Exception("Can't create database ($name): ". $e->getMessage());
                    }
                    $db = $this->service('Database', $name, $code_conf);
                }
                else {
                    $db_select = 'select db.db_id, db.owner_group_id, db.db_env, db.db_name, db.db_label, db.db_desc, db.db_connection_id, ' .
                                 'dbc.db_instance_id, dbc.db_connection_name, db.db_owner_group_id, dbc.default_schema, ' .
                                 'db.dbtype, db.schema_name, db.status, ' .
                                 'dbc.dbhost, dbc.db_class, dbc.db_class as class, dbc.dsn, dbc.dbuser, dbc.dbpass, dbc.dboptions, ' .
                                 'db.reserved_size, db.est_current_size, db.last_refresh_dttm, db.user_mode, db.group_mode, db.other_mode ' .
                                 'from {auth_schema_}db inner join {auth_schema_}db_connection dbc on dbc.db_connection_id = db.db_connection_id';
                    #echo "Context::database($name, $code_conf)\n";
                    $db_conf = $authdb->get_hash("$db_select where db.db_env = '$db_env' and db.db_name = ? and db.owner_group_id = ? union $db_select where db.db_env = 'prod' and db.db_name = ? and db.other_mode >= 4", array($db_name, $owner_group_id, $db_name));

                    if ($db_conf) $code_conf = array_merge($code_conf, $db_conf);  # NOTE: conf from db overrides conf from code or filed
                    $db = $this->service('Database', $name, $code_conf);
                    if (!isset($db->db_id)) {
                        $conf    = $db->getConf();
                        $admin   = $this->sessionObject('administration', array('class' => 'App\\Administration'));
                        $admin->lookupAndUpdateDatabaseConf($name, $conf);
                        $db->db_id = $conf['db_id'];
                        $db->owner_group_id = $conf['owner_group_id'];
                        if (isset($db_conf)) {
                            $db->db_id = $db_conf['db_id'];
                            $db->owner_group_id = $db_conf['owner_group_id'];
                            $fix_columns = array();
                            $fix_values  = array();
                            foreach ($conf as $key => $value) {
                                if (array_key_exists($db_conf[$key]) && (!isset($db_conf[$key]) || strcmp($conf[$key], $db_conf[$key]) != 0)) {
                                    $fix_columns[] = $key;
                                    $fix_values[]  = $value;
                                }
                            }
                            if (count($fix_columns) > 0) {
                                # TODO
                                $authdb->update('db', array('db_id' => $db->db_id), $fix_columns, $fix_values);
                            }
                        }
                        else {
                            throw new \Exception("ERROR: There is no database defined by that name ($name)");
                        }
                    }
                }
            }
        }
        if ($trace) trace_exit($db);
        return($db);
    }

    /**
     * authentication($name = 'default', $code_conf = null)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function authentication ($name = 'default', $code_conf = null) {
        global $trace;
        if ($trace) trace_entry();
        if (!$code_conf) $code_conf = array('class'=>'App\\Authentication');
        $service = $this->service('Authentication', $name, $code_conf);
        if ($trace) trace_exit($service);
        return($service);
    }

    /**
     * authorization($name = 'default', $code_conf = null)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function authorization ($name = 'default', $code_conf = null) {
        global $trace;
        if ($trace) trace_entry();
        if (!$code_conf) $code_conf = array('class'=>'App\\Authorization');
        $service = $this->service('Authorization', $name, $code_conf);
        if ($trace) trace_exit($service);
        return($service);
    }

    /**
     * serializer($name = 'default', $code_conf = null)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function serializer ($name = 'default', $code_conf = null) {
        global $trace;
        if ($trace) trace_entry();
        if (!$code_conf) $code_conf = array('class'=>'App\\Serializer');
        $service = $this->service('Serializer', $name, $code_conf);
        if ($trace) trace_exit($service);
        return($service);
    }

    /**
     * sessionObject($name = 'default', $code_conf = array())
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function sessionObject ($name = 'default', $code_conf = array()) {
        global $trace;
        if ($trace) trace_entry();
        if (!$code_conf) $code_conf = array();
        $service = $this->service('SessionObject', $name, $code_conf);
        if ($trace) trace_exit($service);
        return($service);
    }

    /**
     * getConsistentUserInfo(&$user, $group_id = APP_ALLUSERS_GROUP_ID, $app_id = APP_DEFAULT_APP_ID)
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function getConsistentUserInfo (&$user, $group_id = APP_ALLUSERS_GROUP_ID, $app_id = APP_DEFAULT_APP_ID) {
        global $trace;
        if ($trace) trace_entry();
        $test_mode = $this->getOption('test_mode');
        $authdb    = $this->authdb();

        if ($user && isset($user['user_id'])) {
            $user_id = $user['user_id'];
            $group_sql = <<<EOF
select
  g.group_id,
  g.group_name
from {auth_schema_}auth_group g
     left join {auth_schema_}auth_group_user_memb gum on gum.group_id = g.group_id and gum.user_id = ?
where g.group_id = ?
  and (g.public_ind = 'Y' or gum.user_id = ?)
EOF;
            list($auth_group_id, $auth_group_name) = $authdb->get_row($group_sql, array($user_id, $group_id, $user_id));
            if (isset($auth_group_id)) {
                $user['group_id']   = $auth_group_id;
                $user['group_name'] = $auth_group_name;
            }
            else {
                $user['group_id']   = APP_ALLUSERS_GROUP_ID;
                $user['group_name'] = APP_ALLUSERS_GROUP_NAME;
            }

            $app_sql = <<<EOF
select
  a.app_id
from {auth_schema_}auth_app a
     left join {auth_schema_}auth_group_app_access gaa on gaa.app_id = a.app_id and gaa.group_id = ?
where a.app_id = ?
  and (a.public_ind = 'Y' or gaa.group_id = ?)
EOF;
            $auth_app_id = $authdb->get($app_sql, array($auth_group_id, $app_id, $auth_group_id));
            $user['app_id'] = (isset($auth_app_id)) ? $auth_app_id : APP_DEFAULT_APP_ID;
        }
        if ($trace) trace_exit($user);
        return($user);
    }

    /**
     * getAuthorizedGroups()
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function getAuthorizedGroups () {
        $authdb = $this->authdb();
        $group_sql = <<<EOF
select
    g.group_id        as group_id,
    g.group_name      as group_name,
    g.group_long_name as group_long_name,
    g.owner_user_id   as owner_user_id,
    g.public_ind      as public_ind
from {auth_schema_}auth_group_user_memb gum
     inner join {auth_schema_}auth_group g on g.group_id = gum.group_id
where gum.user_id = ?
union
select
    g.group_id        as group_id,
    g.group_name      as group_name,
    g.group_long_name as group_long_name,
    g.owner_user_id   as owner_user_id,
    g.public_ind      as public_ind
from {auth_schema_}auth_group g
where g.public_ind = 'Y'
order by
    public_ind desc,
    group_id
EOF;
        $user_id  = $this->authenticated_user_id;
        $groups = $authdb->get_hashes($group_sql, array($user_id));
        if (!$groups || count($groups) == 0)
            throw new \Exception("ERROR: There are no groups for user_id ($user_id)");
        return($groups);
    }

    /**
     * getAuthorizedApps()
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function getAuthorizedApps () {
        $authdb = $this->authdb();
        $app_sql = <<<EOF
select
    a.app_id     as app_id,
    a.app_cd     as app_cd,
    a.app_name   as app_name,
    a.public_ind as public_ind
from {auth_schema_}auth_group_app_access gaa
     inner join {auth_schema_}auth_app a on a.app_id = gaa.app_id
where gaa.group_id = ?
union
select
    a.app_id     as app_id,
    a.app_cd     as app_cd,
    a.app_name   as app_name,
    a.public_ind as public_ind
from {auth_schema_}auth_app a
where a.public_ind = 'Y'
order by
    public_ind desc,
    app_id
EOF;
        $apps = $authdb->get_hashes($app_sql, array($this->authenticated_group_id));
        return($apps);
    }

    /**
     * getAuthorizedPerms()
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function getAuthorizedPerms () {
        if (!isset($this->authorized_perms)) {
            $authdb = $this->authdb();
            $this->authorized_perms = array();
            $perm_sql = <<<EOF
select
    p.perm_cd,
    max(prg.is_granted) as is_granted
from {auth_schema_}auth_role_user_grant rug
     inner join {auth_schema_}auth_perm_role_grant prg on prg.role_id = rug.role_id
     inner join {auth_schema_}auth_perm p on p.perm_id = prg.perm_id
where rug.user_id = ?
  and rug.group_id = ?
  and rug.status = 'A'
  and prg.group_id = ?
  and p.app_id = ?
group by p.perm_cd
order by p.perm_cd
EOF;
            $rows = $authdb->get_rows($perm_sql, array($this->authenticated_user_id, $this->authenticated_group_id, $this->authenticated_group_id, $this->authenticated_app_id));

            if (count($rows) === 0) {

                if ($this->authenticated_group_id == APP_ALLUSERS_GROUP_ID) {
                    $role_id = APP_OBSERVER_ROLE_ID;
                }
                elseif ($this->authenticated_user_id != APP_GUEST_USER_ID && $this->authenticated_user_id == $this->authenticated_group_id) {
                    $role_id = APP_ADMIN_ROLE_ID;
                }
                else {
                    $role_id = APP_COLLABORATOR_ROLE_ID;
                }

                $perm_sql = <<<EOF
select
    p.perm_cd,
    max(prg.is_granted) as is_granted
from {auth_schema_}auth_perm_role_grant prg
     inner join {auth_schema_}auth_perm p on p.perm_id = prg.perm_id
where prg.group_id = 1
  and prg.role_id = ?
  and p.app_id = ?
group by p.perm_cd
order by p.perm_cd
EOF;
                $rows = $authdb->get_rows($perm_sql, array($role_id, $this->authenticated_app_id));
            }
            if (count($rows) > 0) {
                foreach ($rows as $row) {
                    $this->authorized_perms[$row[0]] = $row[1];
                }
            }
            else {
                $this->authorized_perms = array('/' => 0);
            }
        }
        return($this->authorized_perms);
    }

    /**
     * getAuthorizedDatabases()
     * @param array $request
     * @param array &$response
     * @return void
     */
    public function getAuthorizedDatabases () {
        if (!isset($this->authorized_databases)) {
            $authdb    = $this->authdb();
            $db_env    = $this->db_env();
            $group_id  = $this->getAuthenticatedGroupId();
            $db_select = <<<EOF
select
    db.owner_group_id,
    db.db_env,
    db.db_name,
    db.db_label,
    db.db_desc,
    db.db_owner_group_id,
    db.dbtype,
    db.status,
    db.reserved_size,
    db.est_current_size,
    db.user_mode,
    db.group_mode,
    db.other_mode,
    dbo.user_mode  as dbo_user_mode,
    dbo.group_mode as dbo_group_mode,
    dbo.other_mode as dbo_other_mode
from {auth_schema_}db
     inner join {auth_schema_}db_connection dbc on dbc.db_connection_id = db.db_connection_id
     left  join {auth_schema_}db dbo on dbo.owner_group_id = db.db_owner_group_id
                                    and dbo.db_env = db.db_env
                                    and dbo.db_name = db.db_name
where db.db_env = ?
  and db.owner_group_id = ?
  and db.db_name not in ('auth','members','log','meta')
order by db.db_name
EOF;
            # left  join {auth_schema_}db dbo on dbo.db_id = db.db_owner_db_id
            $this->authorized_databases = $authdb->get_hash_of_hashes_by_key($db_select, array($db_env, $group_id), null, 'db_name');
        }
        return($this->authorized_databases);
    }

    /**
     * noCache()
     * @param array $request
                if ($this->authenticated_group_id == APP_ALLUSERS_GROUP_ID) {
                    $role_id = APP_OBSERVER_ROLE_ID;
                }
                $role_id = ($this->authenticated_user_id != APP_GUEST_USER_ID && $this->authenticated_user_id == $this->authenticated_group_id) ? 1 : 2; # 1=admin, 2=member
                $perm_sql = <<<EOF
select
    p.perm_cd,
    max(prg.is_granted) as is_granted
from {auth_schema_}auth_perm_role_grant prg
     inner join {auth_schema_}auth_perm p on p.perm_id = prg.perm_id
where prg.group_id = 1
  and prg.role_id = ?
  and p.app_id = ?
group by p.perm_cd
order by p.perm_cd
EOF;
                $rows = $authdb->get_rows($perm_sql, array($role_id, $this->authenticated_app_id));
            }
            if (count($rows) > 0) {
                foreach ($rows as $row) {
                    $this->authorized_perms[$row[0]] = $row[1];
                }
            }
            else {
                $this->authorized_perms = array('/' => 0);
            }
        }
        return($this->authorized_perms);
    }

    /**
     * noCache()
     * @param array $request
     * @param array &$response
     * @return void
     */
    # This is useful for HTML pages that have dynamic HTML / javascript (such as Single Page Applications)
    # If no-cache headers are set properly on this page, pressing the BACK button to go back to it will cause the page to be reloaded from the server.
    # Important to synchronize (1) the ff-session cookie, (2) the current running Javascript (including configs loaded which may duplicate the ff-session values), (3) the displayed HTML
    # Reference: http://www.mnot.net/cache_docs/
    # Reference: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
    public function noCache () {
        global $trace;
        if ($trace) trace_entry();
        if (!$this->no_cache) {
            header('Cache-Control: no-cache, no-store, max-age=0, must-revalidate'); // HTTP 1.1. (perhaps max-age=0 is redundant with must-revalidate, but no harm)
            header('Pragma: no-cache');                         // HTTP 1.0.
            header('Expires: Thu, 01 Jan 1970 00:00:01 GMT');   // Proxies. (Any past date would work. Here I chose 1 second past the epoch/origin of Unix time.)
            header('Vary: *');                                  // IE 8
            $this->no_cache = 1;
        }
        if ($trace) trace_exit();
    }

    /**
     * mkdir($dir)
     * @param array $request
     * @param array &$response
     * @return void
     */
    #NOTE: This is supposed to do the job, but it doesn't set the modes correctly.
    #          mkdir($dir, 02775, true);
    #      So I wrote this method to do the job.
    public function mkdir ($dir) {
        #echo "####################### mkdir($dir) ########################\n";
        $dir_parts = explode('/', $dir);
        for ($i = 0; $i < count($dir_parts); $i++) {
            $newdir = ($i == 0) ? (($dir_parts[0] === '') ? '/' : $dir_parts[0]) : $newdir.'/'.$dir_parts[$i];
            if (!is_dir($newdir)) {
                if (!mkdir($newdir, 02775)) throw new \Exception("Error: cannot create directory ($newdir)");
                if (!chmod($newdir, 02775)) throw new \Exception("Error: cannot set permissions on directory ($newdir)");
            }
        }
    }

    /**
     * rmdir($dir) - recursive directory removal
     */
    public function rmdir ($dir) {
        global $options;
        if ($dir && is_dir($dir)) {
            $prefix = $options['prefix'];
            if (preg_match("!^$prefix/data/fs/[0-9]!", $dir)) {
                $files = array_diff(scandir($dir), array('.','..')); 
                foreach ($files as $file) { 
                    (is_dir("$dir/$file")) ? $this->rmdir("$dir/$file") : unlink("$dir/$file"); 
                } 
                return rmdir($dir); 
            }
        }
        return false;
    }

    public function __toString() {
        global $trace;
        if ($trace) trace_entry();
        $str = "[Context]";
        if ($trace) trace_exit($str);
        return($str);
    }
}

